Skip to main content

GeneralUpdate.Avalonia.Android Cookbook

This guide is for developers who need to integrate Android APK auto-update into their Avalonia applications. The goal is to complete the full loop — "query the server → download APK → verify SHA256 → launch the installer → confirm the installation on next launch" — with minimal code.

Prerequisites

This guide assumes you already have an Avalonia project configured for Android target framework. If not, please refer to the Avalonia documentation first.

Update Flow​

① Version query (inside the component)  ② Download + verify     ③ Install + confirm
┌──────────────┐ ┌──────────┐ ┌──────────────────┐
│ Client │──POST──────→│ Server │ │ Android │
│ (Avalonia) │←──JSON──────│ (Update │ │ Package │
└──────┬───────┘ │ Service) │ │ Installer │
│ └────┬─────┘ └────────┬─────────┘
│ ValidateAsync(currentVersion) — the component sends the request
│ and receives PackageInfo (version/url/hash/size/…)
│ │ │
│ GET /app-v2.0.0.apk (Range supported, resumable) │
│ ────────────────────────→│ │
│ APK stream ←────────────│ │
│ │ │
│ After size + SHA256 verification, persist record │
│ launch Package Installer ───────────────────────────→ │
│ │ │
│ ★ Next launch: CheckInstallationAsync reconciles ←────│
▼ ▼ ▼
RoleDefinitionResponsibilities
Client (Avalonia)Your Avalonia Android appQuery version → download APK → verify → launch installer → confirm result
ServerUpdate serviceReturn version metadata, serve the APK with Range support
Android Package InstallerAndroid system componentInstall the APK (verify signature, replace the app)

Phase 1: Environment Setup​

Requirements​

ItemRequirementVerify
.NET SDK10.0+dotnet --version
Android workloadInstalleddotnet workload list (should include android)
Android SDKAPI 26+Android Studio SDK Manager
Avalonia12+Avalonia package version referenced by your project
Target frameworknet10.0-androidTargetFramework in .csproj

NuGet Package​

dotnet add package GeneralUpdate.Avalonia.Android

Phase 2: Android Integration Configuration​

The host must configure all three items below; missing any one of them stops the update half way.

2.1 Declare the install permission (Android 8.0+)​

In AndroidManifest.xml:

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

The user may still not have granted it. In that case LaunchInstallerAsync returns FailureReason = InstallPermissionDenied. Use the following code to send the user to "install unknown apps", then retry:

var context = Android.App.Application.Context;
context.StartActivity(new Android.Content.Intent(
Android.Provider.Settings.ActionManageUnknownAppSources,
Android.Net.Uri.Parse("package:" + context.PackageName))
.AddFlags(Android.Content.ActivityFlags.NewTask));

2.2 Configure FileProvider​

The APK must be handed to the Android Package Installer through a FileProvider. authorities must match the FileProviderAuthority used in code:

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.app.generalupdate.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/generalupdate_file_paths" />
</provider>

Create Resources/xml/generalupdate_file_paths.xml; the paths must cover the download directory (default <CacheDir>/update):

<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="update_cache" path="update/" />
<files-path name="update_files" path="update/" />
</paths>

Note: if the authority or paths do not match, LaunchInstallerAsync returns InstallLaunchFailed.

Plaintext HTTP is for local demos only

The component rejects non-HTTPS APK downloads by default (returning InvalidMetadata). Only set AllowInsecureHttpDownloads = true in a trusted development environment (for example http://127.0.0.1:5080), together with android:usesCleartextTraffic="true". Always use HTTPS in production.

Phase 3: Configure the Server Query​

Package metadata is no longer built by the caller; the component queries it according to AndroidUpdateOptions.UpdateServer:

var options = new AndroidUpdateOptions
{
FileProviderAuthority = "com.example.app.generalupdate.fileprovider",
DownloadDirectoryPath = Path.Combine(
Android.App.Application.Context.CacheDir!.AbsolutePath!, "update"),

UpdateServer = new UpdateServerOptions
{
RequestUrl = "https://example.com/Upgrade/Verification",
AppKey = "your-app-key",
AppType = 1,
Platform = androidPlatformId, // must match your deployment; never hardcode
ProductId = "your-product-id"
}
};

Two supported protocols:

ModeConfigurationBehavior
GeneralUpdate verification protocol (default)UseJsonEndpoint = falsePOST body version/appKey/appType/platform/productId, response {"code":200,"body":[...]}; the component picks the newest non-frozen full APK
Static JSON endpointUseJsonEndpoint = trueGET a single UpdatePackageInfo JSON document; HTTP 204 or null means no update

Example static JSON endpoint:

{
"version": "2.3.0",
"downloadUrl": "https://example.com/app-release.apk",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"description": "Release notes",
"isForced": false
}

sha256 must be the real 64-character hexadecimal SHA-256 of the APK (not MD5); fileSize may be omitted or 0.

If your server speaks a completely different protocol, implement IUpdatePackageSource.GetLatestAsync(currentVersion, ct) and inject it with CreateDefault(options, packageSource: mySource) — no server changes required.

Phase 4: Write the Update Code​

In an Avalonia ViewModel or service:

using GeneralUpdate.Avalonia.Android;
using GeneralUpdate.Avalonia.Android.Models;
using GeneralUpdate.Avalonia.Android.Services;

public sealed class UpdateService
{
private IAndroidBootstrap? _bootstrap;

public void Initialize(string fileProviderAuthority, string token)
{
var options = new AndroidUpdateOptions
{
FileProviderAuthority = fileProviderAuthority,
DownloadDirectoryPath = Path.Combine(
Android.App.Application.Context.CacheDir!.AbsolutePath!, "update"),
UpdateServer = new UpdateServerOptions
{
RequestUrl = "https://example.com/Upgrade/Verification",
AppKey = "your-app-key",
Platform = androidPlatformId,
ProductId = "your-product-id"
}
};

_bootstrap = GeneralUpdateBootstrap.CreateDefault(
options,
httpOptions: new HttpDownloadOptions
{
AuthProvider = new BearerTokenAuthProvider(token),
MaxRetryAttempts = 3,
RetryBaseDelay = TimeSpan.FromSeconds(1)
});

_bootstrap.AddListenerDownloadProgressChanged += (_, e) =>
Console.WriteLine($"Download: {e.ProgressPercentage:F1}% {e.StatusDescription}");

_bootstrap.AddListenerUpdateFailed += (_, e) =>
Console.WriteLine($"Failed: {e.Result.Message} ({e.Result.FailureReason})");

_bootstrap.AddListenerInstallationConfirmed += (_, e) =>
Console.WriteLine($"Installation confirmed: target {e.Result.Record?.TargetVersion}, " +
$"installed {e.Result.CurrentVersion}");
}

/// <summary>Call at startup or after returning from the installer: offline reconciliation.</summary>
public async Task CheckInstallationAsync()
{
if (_bootstrap is null) return;
var installation = await _bootstrap.CheckInstallationAsync(ReadInstalledVersion());
Console.WriteLine($"State={installation.State} Installed={installation.IsInstalled} " +
$"Pending={installation.HasPendingInstallation}");
}

/// <summary>Check and prepare the package; the caller confirms before installing.</summary>
public async Task<(UpdatePackageInfo Package, string FilePath)?> PrepareAsync()
{
if (_bootstrap is null) return null;

var prepared = await _bootstrap.PrepareUpdateAsync(ReadInstalledVersion());
if (!prepared.Success)
{
Console.WriteLine($"Preparation failed: {prepared.Message}");
return null;
}

if (!prepared.UpdateFound)
{
Console.WriteLine("Already up to date.");
return null;
}

// Install permission check (Android 8+); guide the user when not granted
if (!HasInstallPermission())
{
OpenUnknownSourcesSettings();
return null;
}

return prepared.IsReadyToInstall && prepared.PackageInfo is { } p && prepared.FilePath is { } path
? (p, path)
: null;
}

public async Task InstallAsync(UpdatePackageInfo package, string filePath)
{
if (_bootstrap is null) return;
var install = await _bootstrap.LaunchInstallerAsync(package, filePath);
Console.WriteLine(install.Success
? "Installer launched. Confirm the installation on the device."
: $"Failed to launch installer: {install.Message}");
}

private static string ReadInstalledVersion()
{
var context = Android.App.Application.Context;
return context.PackageManager?.GetPackageInfo(
context.PackageName!, Android.Content.PM.PackageInfoFlags.Activities)?.VersionName
?? throw new InvalidOperationException("Cannot read the installed version.");
}

private static bool HasInstallPermission()
{
if (Android.OS.Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.O) return true;
var pm = Android.App.Application.Context.PackageManager;
return pm is not null && pm.CanRequestPackageInstalls();
}

private static void OpenUnknownSourcesSettings()
{
var ctx = Android.App.Application.Context;
ctx.StartActivity(new Android.Content.Intent(
Android.Provider.Settings.ActionManageUnknownAppSources,
Android.Net.Uri.Parse("package:" + ctx.PackageName))
.AddFlags(Android.Content.ActivityFlags.NewTask));
}
}

PrepareUpdateAsync performs the query, version comparison, pre-check, download and SHA256 verification under a single operation lock. It never opens the installer or a permission page.

4.2 Phased style (when you need intermediate UI)​

// ① Check the version only
var check = await bootstrap.ValidateAsync(currentVersion);
if (!check.UpdateFound || check.PackageInfo is null) return;
Console.WriteLine($"New version {check.TargetVersion}: {check.PackageInfo.Description}");

// ② After user confirmation, download and verify
var download = await bootstrap.DownloadAndVerifyAsync(check.PackageInfo);
if (!download.Success || download.FilePath is null) return;
Console.WriteLine("Downloaded and verified. Ready to install.");

// ③ Launch the installer
await bootstrap.LaunchInstallerAsync(check.PackageInfo, download.FilePath);

4.3 Update pre-check (business interception)​

bootstrap.AddListenerUpdatePrecheck(args =>
{
// Skip non-forced updates while not on Wi-Fi
if (!IsWifiConnected())
{
Console.WriteLine($"Waiting for Wi-Fi before updating to {args.PackageInfo.Version}");
return true; // true = skip; ValidateAsync returns UpdateFound = false
}

return false; // false = continue
});

Forced updates (UpdatePackageInfo.IsForced = true) never invoke this callback.

Phase 5: One-Command End-to-End Demo​

The GeneralUpdate.Avalonia repository ships a runnable Android sample. Connect a USB-debugging device or start an emulator, then run:

samples\GeneralUpdate.Avalonia.Android.Sample\run-demo.cmd

The script checks .NET 10 and the Android workload, builds the 1.0.0 and 2.0.0 APKs, starts a local demo server that implements the GeneralSpacestation verification protocol (http://127.0.0.1:5080), maps the port with adb reverse tcp:5080 tcp:5080, and finally installs and launches 1.0.0.

Fixed parameters of the demo server:

ParameterValue
RequestUrlhttp://127.0.0.1:5080/Upgrade/Verification
AppKeydemo-client
ProductIddemo-product
Platform3
AppType1

To use the GeneralUpdate sample server instead, run the Server project from this Samples repository:

git clone https://github.com/GeneralLibrary/GeneralUpdate-Samples.git
cd GeneralUpdate-Samples/src/Server
dotnet run

The server listens on http://localhost:5000 by default. On an Android emulator use http://10.0.2.2:5000 to reach the host's localhost.

Phase 6: End-to-End Verification​

  1. Tap "Check and auto update" in the app
  2. Allow "install unknown apps" when Android asks
  3. Confirm the installation of 2.0.0 in the system installer
  4. Reopen the app and confirm "Current Version" is now 2.0.0 and the result reads "installation confirmed"

Expected Output​

Checking for updates...
Update v2.0.0 found.
Downloading 45% 1.2 MB/s
Downloading 100% Download completed
SHA256 verified.
Installer launched.
(after installation, reopen the app)
Installation confirmed: target 2.0.0, installed 2.0.0
State=Installed Installed=True Pending=False

Acceptance Checklist​

ScenarioExpected behavior
Cancel the installation in the system installerThe app stays on the old version, HasPendingInstallation = true, retry is possible
Return after granting the permissionServer configuration and target are preserved; download and install can be retried
Open the app offline after installingOffline reconciliation still confirms the upgrade; a server query failure is reported separately and does not overwrite the confirmed result
Reopen the already updated appShows the previous confirmation but does not raise AddListenerInstallationConfirmed again
Corrupt installation recordReported as an explicit error and the update stops; the user clears it with ResetInstallationAsync()

Troubleshooting​

SymptomCauseAction
FailureReason = InstallPermissionDenied"Install unknown apps" not grantedSend the user to Settings.ActionManageUnknownAppSources, then retry
FailureReason = InstallLaunchFailedFileProvider authority or paths do not match the download directoryCheck AndroidManifest.xml and generalupdate_file_paths.xml
FailureReason = InvalidMetadataNo UpdateServer configured, metadata missing URL/SHA256, or the download URL is not HTTPSCheck the server response fields and HTTPS configuration
FailureReason = NetworkErrorNetwork failure/timeout during query or downloadCheck the network, RequestTimeout / DownloadTimeout and retry settings
FailureReason = VersionComparisonFailedVersion string is not a valid System.VersionUse semantic versions or replace IVersionComparer
HashMismatchServer sha256 does not match the actual APK hashRecompute the 64-character hexadecimal SHA-256 of the APK
Every attempt re-downloads the whole APKThe server does not support RangeMake the server return Accept-Ranges: bytes
Installation confirmation is not proof of a completed install

LaunchInstallerAsync returning Success = true only means the installer intent was launched. Only CheckInstallationAsync returning IsInstalled means the device actually reached the target version. The APK must keep the same package name, a compatible signature and an increased versionCode; the component provides no silent install, no auto-restart, no health check and no rollback.

Next Steps​